File: /var/www/kevin-demo/wp-content/plugins/allspice/includes/settings.php
<?php
// includes/settings.php
if (!defined('ABSPATH')) exit;
define('ALLSPICE_SETTINGS_PAGE', 'allspice-settings');
define('ALLSPICE_SETTINGS_GROUP', 'allspice_settings_group');
define('ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION', 'allspice_sync_buttons_settings');
define('ALLSPICE_SYNC_RECIPES_ACTION', 'allspice_sync_recipes');
define('ALLSPICE_SYNC_ALL_ACTION', 'allspice_sync_all');
$css = '
.allspice-admin{
--asx-solid:#F9705F;
--asx-g1:#F25B57;
--asx-g2:#FF8266;
--asx-text:#303030;
--asx-text2:#606060;
--asx-text3:#B0B0B0;
--asx-bg:#FFFFFF;
--asx-bg2:#F2F2F2;
--asx-tint:#EAEAEA;
--asx-blue:#0099FF;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
}
.allspice-admin .description{ color: var(--asx-text2); }
.allspice-admin__stack{
max-width: 980px;
margin: 14px 0 18px 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.allspice-admin__card{
background: var(--asx-bg);
border: 1px solid rgba(194,194,194,0.38);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.allspice-admin__accent{
height: 6px;
background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2));
}
.allspice-admin__body{ padding: 18px; }
.allspice-admin__sectionTitle{
margin: 0 0 10px 0;
color: var(--asx-text);
font-weight: 900;
font-size: 14px;
letter-spacing: 0.2px;
}
.allspice-admin__kicker{
margin: 0 0 10px 0;
color: var(--asx-text3);
font-weight: 900;
letter-spacing: 0.10em;
font-size: 11px;
}
.allspice-admin__divider{
height: 1px;
background: rgba(234,234,234,0.95);
margin: 16px 0;
}
.allspice-admin .form-table th{
color: var(--asx-text);
font-weight: 900;
padding-top: 12px;
}
.allspice-admin .form-table td{
color: var(--asx-text2);
padding-top: 12px;
}
.allspice-admin .form-table .description{
margin: 6px 0 0 0;
font-size: 12.5px;
line-height: 1.45;
}
.allspice-admin input.regular-text,
.allspice-admin input.large-text,
.allspice-admin input[type="text"],
.allspice-admin input[type="password"]{
border: 1px solid rgba(194,194,194,0.55);
border-radius: 10px;
padding: 8px 10px;
box-shadow: none;
}
.allspice-admin input:focus{
border-color: rgba(249,112,95,0.70);
box-shadow: 0 0 0 3px rgba(249,112,95,0.18);
}
.allspice-admin .button,
.allspice-admin .button-secondary,
.allspice-admin .button-primary{
border-radius: 10px !important;
font-weight: 900 !important;
}
.allspice-admin .button-primary{
background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2)) !important;
border: 0 !important;
box-shadow: 0 6px 14px rgba(242,91,87,0.20) !important;
text-shadow: none !important;
}
.allspice-admin .button-primary:hover{ filter: brightness(0.98); }
.allspice-admin .button-secondary{
border-color: rgba(194,194,194,0.55) !important;
}
.allspice-admin .button:focus,
.allspice-admin .button-primary:focus,
.allspice-admin .button-secondary:focus{
box-shadow: 0 0 0 3px rgba(249,112,95,0.18) !important;
border-color: rgba(249,112,95,0.70) !important;
}
.allspice-admin__card .notice,
.allspice-admin__card .updated,
.allspice-admin__card .error,
.allspice-admin__card .wrap > .notice,
.allspice-admin__card .wrap > .updated,
.allspice-admin__card .wrap > .error{
display:none !important;
}
.allspice-mkt{ max-width: 980px; margin: 0; }
.allspice-mkt__card{
background: var(--asx-bg);
border: 1px solid rgba(194,194,194,0.38);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.allspice-mkt__accent{ height: 6px; background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2)); }
.allspice-mkt__head{
display:flex;
align-items:center;
justify-content:space-between;
gap:14px;
padding: 14px 18px 0 18px;
}
.allspice-mkt__brand{
display:flex;
align-items:center;
gap:12px;
min-width: 0;
}
.allspice-mkt__brand img{ height: 28px; width:auto; display:block; }
.allspice-mkt__tag{
color: var(--asx-text2);
font-size: 12.5px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.allspice-mkt__ctaTop{
display:flex;
gap:10px;
flex-wrap:wrap;
justify-content:flex-end;
}
.allspice-mkt__btnPrimary{
display:inline-flex;
align-items:center;
justify-content:center;
padding: 9px 12px;
border-radius: 10px;
border: 0;
text-decoration:none;
color:#fff !important;
background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2));
font-weight: 900;
font-size: 13px;
box-shadow: 0 6px 14px rgba(242,91,87,0.22);
}
.allspice-mkt__btnPrimary:hover{ filter: brightness(0.98); transform: translateY(-0.5px); }
.allspice-mkt__btnSecondary{
display:inline-flex;
align-items:center;
justify-content:center;
padding: 9px 12px;
border-radius: 10px;
border: 1px solid rgba(234,234,234,0.95);
text-decoration:none;
color: var(--asx-text) !important;
background:#fff;
font-weight: 900;
font-size: 13px;
}
.allspice-mkt__btnSecondary:hover{ border-color: rgba(249,112,95,0.55); color: var(--asx-solid) !important; }
.allspice-mkt__inner{
display: grid;
grid-template-columns: 1.25fr 0.75fr;
gap: 18px;
padding: 18px;
}
@media (max-width: 900px){ .allspice-mkt__inner{ grid-template-columns: 1fr; } }
.allspice-mkt__leftTitle{
margin:0;
color: var(--asx-text3);
font-weight: 900;
letter-spacing: 0.10em;
font-size: 11px;
}
.allspice-mkt__grid{
margin-top: 10px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 900px){ .allspice-mkt__grid{ grid-template-columns: 1fr; } }
.allspice-mkt__feature{
background: rgba(242,242,242,0.55);
border: 1px solid rgba(234,234,234,0.95);
border-radius: 12px;
padding: 10px 12px;
}
.allspice-mkt__featureTitle{
margin:0;
color: var(--asx-text);
font-weight: 900;
font-size: 13px;
letter-spacing: 0.2px;
}
.allspice-mkt__featureBody{
margin: 4px 0 0 0;
color: var(--asx-text2);
font-size: 13px;
line-height: 1.55;
}
.allspice-mkt__right{
border-left: 1px solid rgba(234,234,234,0.9);
padding-left: 18px;
}
@media (max-width: 900px){
.allspice-mkt__right{
border-left: none;
padding-left: 0;
border-top: 1px solid rgba(234,234,234,0.9);
padding-top: 14px;
}
}
.allspice-mkt__stepsTitle{
margin:0;
color: var(--asx-text3);
font-weight: 900;
letter-spacing: 0.10em;
font-size: 11px;
}
.allspice-mkt__steps{
margin: 10px 0 0 0;
padding-left: 18px;
color: var(--asx-text2);
font-size: 13px;
line-height: 1.55;
}
.allspice-mkt__steps li{ margin: 6px 0; }
.allspice-mkt__steps strong{ color: var(--asx-text); font-weight: 900; }
.allspice-mkt__note{
margin-top: 10px;
padding: 10px 12px;
background: rgba(242,242,242,0.60);
border: 1px solid rgba(234,234,234,0.95);
border-radius: 12px;
color: var(--asx-text2);
font-size: 12.5px;
line-height: 1.5;
}
.allspice-mkt__note a{ color: var(--asx-blue); text-decoration:none; font-weight:900; }
.allspice-mkt__note a:hover{ text-decoration: underline; }
.allspice-premium-banner{
margin: 16px 0 0 0;
padding: 14px 16px;
border-radius: 14px;
border: 1px solid rgba(249,112,95,0.35);
background: rgba(249,112,95,0.08);
display:flex;
align-items:center;
justify-content:space-between;
gap:14px;
}
.allspice-premium-banner__title{
margin:0;
color: var(--asx-text);
font-weight:900;
font-size:14px;
}
.allspice-premium-banner__body{
margin:4px 0 0 0;
color: var(--asx-text2);
font-size:13px;
line-height:1.45;
max-width:700px;
}
.allspice-premium-banner__button{
display:inline-flex;
align-items:center;
justify-content:center;
white-space:nowrap;
padding:9px 12px;
border-radius:10px;
background:linear-gradient(90deg, var(--asx-g1), var(--asx-g2));
color:#fff !important;
text-decoration:none;
font-weight:900;
box-shadow:0 6px 14px rgba(242,91,87,0.18);
}
.allspice-premium-banner__button:hover{
filter:brightness(0.98);
}
@media (max-width: 700px){
.allspice-premium-banner{
align-items:flex-start;
flex-direction:column;
}
}
.allspice-sync-actions{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
margin-top:12px;
}
.allspice-sync-actions form{
margin:0;
}
';
add_action('admin_enqueue_scripts', function ($hook) use ($css) {
if ($hook !== 'settings_page_' . ALLSPICE_SETTINGS_PAGE) return;
wp_register_style('allspice-admin-settings', false, [], '1.0.0');
wp_enqueue_style('allspice-admin-settings');
wp_add_inline_style('allspice-admin-settings', $css);
});
add_action('current_screen', function ($screen) {
if (!$screen || $screen->id !== 'settings_page_' . ALLSPICE_SETTINGS_PAGE) return;
remove_all_actions('admin_notices');
remove_all_actions('all_admin_notices');
remove_all_actions('network_admin_notices');
remove_all_actions('user_admin_notices');
add_action('admin_notices', function () {
$n = get_transient('allspice_admin_notice');
if (!$n || empty($n['msg'])) return;
delete_transient('allspice_admin_notice');
$type = in_array(($n['type'] ?? ''), ['success','error','warning','info'], true)
? (string)$n['type']
: 'info';
echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible">';
echo '<p>' . esc_html((string)$n['msg']) . '</p>';
echo '</div>';
}, 1);
});
function allspice_admin_card_open(string $kicker, string $title = '', bool $accent = false): void {
echo '<div class="allspice-admin__card">';
if ($accent) echo '<div class="allspice-admin__accent"></div>';
echo '<div class="allspice-admin__body">';
if ($kicker !== '') echo '<p class="allspice-admin__kicker">' . esc_html($kicker) . '</p>';
if ($title !== '') echo '<h2 class="allspice-admin__sectionTitle">' . esc_html($title) . '</h2>';
}
function allspice_admin_card_close(): void {
echo '</div></div>';
}
function allspice_render_marketing_section(array $s): void {
$portal_base = 'https://www.allspicelabs.com';
$login_url = $portal_base . '/login/';
$register_url = $portal_base . '/signup/';
$logo_url = 'https://firebasestorage.googleapis.com/v0/b/allspice-f3bfd.firebasestorage.app/o/hosted_assets%2Fallspice_combined_red_full_registered.png?alt=media';
echo '<div class="allspice-mkt"><div class="allspice-mkt__card"><div class="allspice-mkt__accent"></div>';
echo '<div class="allspice-mkt__head">';
echo '<div class="allspice-mkt__brand"><img src="' . esc_url($logo_url) . '" alt="Allspice" loading="lazy"/><div class="allspice-mkt__tag">Tools that work with your existing site</div></div>';
echo '<div class="allspice-mkt__ctaTop">';
echo '<a class="allspice-mkt__btnPrimary" href="' . esc_url($login_url) . '" target="_blank" rel="noopener noreferrer">Manage in Publisher Portal</a>';
echo '<a class="allspice-mkt__btnSecondary" href="' . esc_url($register_url) . '" target="_blank" rel="noopener noreferrer">Create account</a>';
echo '</div></div>';
echo '<div class="allspice-mkt__inner">';
echo '<div><p class="allspice-mkt__leftTitle">HOW IT WORKS</p><div class="allspice-mkt__grid">';
echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Save & Organize Recipes</p><p class="allspice-mkt__featureBody">Save recipes across any site in our ecosystem. The only platform that lets readers build a collection from multiple recipe sites.</p></div>';
echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Smart Grocery Lists</p><p class="allspice-mkt__featureBody">AI-powered grocery lists that understand ingredients, suggest substitutions, and integrate with shopping services.</p></div>';
echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Create Meal Plans</p><p class="allspice-mkt__featureBody">Plan entire weeks of meals from your recipes. Readers organize dinners, get suggestions, and send lists to Amazon Fresh.</p></div>';
echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Track Pantry</p><p class="allspice-mkt__featureBody">Check what readers already have, get substitution suggestions, and alerts for items running low or expiring soon.</p></div>';
echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Cook Mode</p><p class="allspice-mkt__featureBody">Step-by-step view for phone-in-hand cooking. No more scrolling between ingredients and instructions.</p></div>';
echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">AI Chat Agent</p><p class="allspice-mkt__featureBody">Intelligent cooking assistant that helps customize recipes, troubleshoot issues, and answer questions in real-time.</p></div>';
echo '</div></div>';
echo '<div class="allspice-mkt__right"><p class="allspice-mkt__stepsTitle">SETUP</p><ol class="allspice-mkt__steps">';
echo '<li>Log in to the Publisher Portal</li>';
echo '<li>Add your site to get a <strong>Domain ID</strong></li>';
echo '<li>Copy your <strong>Publisher ID</strong> + <strong>Webhook Token</strong> into this page</li>';
echo '<li>Click <strong>Sync All</strong></li>';
echo '</ol><div class="allspice-mkt__note">Don't have an account yet? Create one at <a href="' . esc_url($register_url) . '" target="_blank" rel="noopener noreferrer">https://www.allspicelabs.com/signup/</a> to get your Publisher ID.</div></div>';
echo '</div></div></div>';
}
function allspice_render_premium_banner(array $s): void {
$premium = (bool)($s['premium'] ?? true);
if ($premium) return;
$portal_url = 'https://portal.allspicelabs.com/login/';
echo '<div class="allspice-premium-banner">';
echo '<div>';
echo '<p class="allspice-premium-banner__title">Premium subscription required</p>';
echo '<p class="allspice-premium-banner__body">This site is connected to Allspice, but features are not active for this domain until billing setup is completed in the Publisher Portal.</p>';
echo '</div>';
echo '<a class="allspice-premium-banner__button" href="' . esc_url($portal_url) . '" target="_blank" rel="noopener noreferrer">Open Publisher Portal</a>';
echo '</div>';
}
add_action('admin_menu', function () {
$hook = add_options_page(
'Allspice',
'Allspice',
'manage_options',
ALLSPICE_SETTINGS_PAGE,
'allspice_render_settings_page'
);
add_action('load-' . $hook, function () {
nocache_headers();
});
});
add_action('admin_init', function () {
register_setting(
ALLSPICE_SETTINGS_GROUP,
ALLSPICE_SETTINGS_OPTION,
['sanitize_callback' => 'allspice_sanitize_settings']
);
add_settings_section(
'allspice_main',
'',
function () {
echo '<p class="description" style="max-width:700px;">Configure your Allspice integration. These settings control how buttons are linked and refreshed from Allspice.</p>';
},
ALLSPICE_SETTINGS_PAGE
);
add_settings_field('partner_id', 'Publisher ID', 'allspice_field_partner_id', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
add_settings_field('domain_id', 'Domain ID', 'allspice_field_domain_id', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
add_settings_field('webhook_token', 'Webhook Token', 'allspice_field_webhook_token', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
add_settings_field('widget_style_desktop', 'Widget Style (Desktop)', 'allspice_field_widget_style_desktop', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
add_settings_field('widget_style_mobile', 'Widget Style (Mobile)', 'allspice_field_widget_style_mobile', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
});
function allspice_sanitize_settings($input): array {
$cur = allspice_opt_get();
$in = is_array($input) ? $input : [];
$out = array_merge($cur, $in);
if (isset($in['partner_id'])) {
$out['partner_id'] = sanitize_text_field((string)$in['partner_id']);
}
if (isset($in['domain_id'])) {
$out['domain_id'] = sanitize_text_field((string)$in['domain_id']);
}
if (isset($in['webhook_token'])) {
$out['webhook_token'] = sanitize_text_field((string)$in['webhook_token']);
}
if (isset($in['widget_variant'])) {
$variant = sanitize_key((string)$in['widget_variant']);
$out['widget_variant'] = in_array($variant, ['standard', 'float'], true) ? $variant : 'standard';
}
/*
* Float launcher appearance. These only affect the float variant - the standard variant's
* floating button and WIDCON-d29x rail never read them.
*/
$allspice_enum = static function ($value, array $allowed, string $fallback): string {
$value = sanitize_key((string)$value);
return in_array($value, $allowed, true) ? $value : $fallback;
};
// Per-device (suffixed) options: style select + full float-family duplication.
foreach (array('desktop', 'mobile') as $allspice_dev) {
$sx = "_{$allspice_dev}";
if (isset($in["widget_style{$sx}"])) {
$v = sanitize_key((string)$in["widget_style{$sx}"]);
$allowed = $allspice_dev === 'mobile'
? array('standard', 'inline')
: array('standard', 'floating', 'inline');
$out["widget_style{$sx}"] = in_array($v, $allowed, true) ? $v : 'standard';
}
foreach (array(
"search_open_target{$sx}" => array(array('full', 'widget', 'advanced_search'), 'full'),
"float_launcher_side{$sx}" => array(array('left', 'right'), 'right'),
"float_rail_anchor{$sx}" => array(array('top', 'middle', 'bottom'), 'middle'),
"float_rail_size{$sx}" => array(array('default', 'small'), 'default'),
"float_rail_mode{$sx}" => array(array('dark', 'light', 'auto'), 'light'),
"float_rail_announce{$sx}" => array(array('zone', 'jump', 'never'), 'zone'),
"float_inline_border_width{$sx}" => array(array('0', '1', '2'), '1'),
"float_inline_radius{$sx}" => array(array('0', '8', '12', '16', '24'), '16'),
"float_inline_pad_y{$sx}" => array(array('8', '14', '22'), '14'),
) as $key => $spec) {
if (isset($in[$key])) {
$v = sanitize_key((string)$in[$key]);
$out[$key] = in_array($v, $spec[0], true) ? $v : $spec[1];
}
}
foreach (array(
"float_rail_accent_top{$sx}" => '#3d7dd6',
"float_rail_accent_ingredients{$sx}" => '#1fa463',
"float_rail_accent_instructions{$sx}" => '#e9781a',
"float_rail_accent_search{$sx}" => '#0284c7',
"float_rail_accent_chat{$sx}" => '#6d4dc4',
"float_inline_bg{$sx}" => '#ffffff',
"float_inline_border_color{$sx}" => '#e7e8ee',
) as $key => $fallback) {
if (isset($in[$key])) {
$hexv = sanitize_hex_color((string)$in[$key]);
$out[$key] = $hexv ?: $fallback;
}
}
foreach (array(
"float_inline_cta{$sx}",
"float_auto_open{$sx}",
"float_rail_announce_search{$sx}",
"float_rail_pulse{$sx}",
"float_rail_hover_expand{$sx}",
"float_rail_show_jump{$sx}",
) as $key) {
if (isset($in[$key])) {
$out[$key] = !empty($in[$key]) ? 1 : 0;
}
}
if (isset($in["float_auto_open_delay{$sx}"]) && is_numeric($in["float_auto_open_delay{$sx}"])) {
$out["float_auto_open_delay{$sx}"] = max(3, min(600, (int)$in["float_auto_open_delay{$sx}"]));
}
if (isset($in["float_rail_announce_ms{$sx}"]) && is_numeric($in["float_rail_announce_ms{$sx}"])) {
$out["float_rail_announce_ms{$sx}"] = max(0, min(15000, (int)$in["float_rail_announce_ms{$sx}"]));
}
foreach (array("float_inline_margin_top{$sx}", "float_inline_margin_bottom{$sx}") as $key) {
if (isset($in[$key]) && is_numeric($in[$key])) {
$out[$key] = max(0, min(60, (int)$in[$key]));
}
}
}
$allspice_float_enums = [
'float_desktop_style' => [['floating', 'inline'], 'floating'],
'float_mobile_style' => [['floating', 'inline'], 'floating'],
'float_inline_border_width' => [['0', '1', '2'], '1'],
'float_inline_radius' => [['0', '8', '12', '16', '24'], '16'],
'float_inline_pad_y' => [['8', '14', '22'], '14'],
'float_launcher_side' => [['left', 'right'], 'right'],
'float_rail_anchor' => [['top', 'middle', 'bottom'], 'middle'],
'float_rail_size' => [['default', 'small'], 'default'],
'float_rail_mode' => [['dark', 'light', 'auto'], 'light'],
'float_rail_announce' => [['zone', 'jump', 'never'], 'zone'],
];
foreach ($allspice_float_enums as $key => $spec) {
if (isset($in[$key])) {
$out[$key] = $allspice_enum($in[$key], $spec[0], $spec[1]);
}
}
foreach ([
'float_rail_accent_top' => '#3d7dd6',
'float_rail_accent_ingredients' => '#1fa463',
'float_rail_accent_instructions' => '#e9781a',
'float_rail_accent_search' => '#0284c7',
'float_rail_accent_chat' => '#6d4dc4',
'float_inline_bg' => '#ffffff',
'float_inline_border_color' => '#e7e8ee',
] as $key => $fallback) {
if (isset($in[$key])) {
$hex = sanitize_hex_color(trim((string)$in[$key]));
$out[$key] = $hex ? $hex : $fallback;
}
}
if (isset($in['float_rail_announce_ms'])) {
$out['float_rail_announce_ms'] = max(0, min(15000, (int)$in['float_rail_announce_ms']));
}
/*
* Unchecked checkboxes post nothing. The renderer emits a hidden companion input for each, so
* the key is always present when the section rendered; absence therefore means the section was
* not on the submitted form and the stored value must be preserved rather than silently cleared.
*/
foreach (['float_rail_announce_search', 'float_rail_pulse', 'float_rail_hover_expand', 'float_rail_show_jump'] as $key) {
if (array_key_exists($key, $in)) {
$out[$key] = !empty($in[$key]);
}
}
return $out;
}
function allspice_field_partner_id(): void {
$s = allspice_opt_get();
echo '<input type="text" class="regular-text" name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[partner_id]" value="' . esc_attr((string)$s['partner_id']) . '"/>';
echo '<p class="description">Get your <strong>Publisher ID</strong> from the <a href="' . esc_url('https://www.allspicelabs.com/login/') . '" target="_blank" rel="noopener noreferrer">Allspice Publisher Portal</a>.</p>';
}
function allspice_field_domain_id(): void {
$s = allspice_opt_get();
echo '<input type="text" class="regular-text" name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[domain_id]" value="' . esc_attr((string)$s['domain_id']) . '"/>';
echo '<p class="description">Identifies this WordPress site (from the Publisher Portal).</p>';
}
function allspice_field_webhook_token(): void {
$s = allspice_opt_get();
echo '<input type="password" class="regular-text" autocomplete="new-password" name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[webhook_token]" value="' . esc_attr((string)$s['webhook_token']) . '"/>';
echo '<p class="description">Shared secret used for secure communication with Allspice.</p>';
}
function allspice_field_widget_variant(): void {
$settings = allspice_opt_get();
$current = allspice_get_widget_variant();
echo '<select name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[widget_variant]">';
echo '<option value="standard" ' .
selected($current, 'standard', false) .
'>Standard widget</option>';
echo '<option value="float" ' .
selected($current, 'float', false) .
'>Floating card widget</option>';
echo '</select>';
echo '<p class="description">';
echo '<strong>Standard:</strong> uses the existing floating button and rail. ';
echo '<strong>Floating card:</strong> uses the adaptive rail launcher.';
echo '</p>';
}
/**
* Collapsible appearance options for the adaptive rail.
*
* The whole row is hidden unless Widget Experience is set to "Floating card", because none of it
* applies to the standard variant. Hiding is done in JS against the live value of the variant
* select so it responds before the page is saved; the row also starts hidden server-side so it
* never flashes for standard sites.
*/
/*
* Per-device widget style section: Standard | Floating | Inline for this device, with every
* relevant option nested underneath and live-toggled by the select:
* floating -> auto-open controls + rail appearance
* inline -> inline block chrome + rail appearance (the rail serves both float-family styles)
* standard -> nothing (legacy experience, no internal settings)
* All option keys are suffixed _desktop/_mobile; reads fall back to the legacy un-suffixed
* values, so existing sites migrate silently.
*/
function allspice_render_device_style_section(string $device): void {
$device = $device === 'mobile' ? 'mobile' : 'desktop';
$sx = "_{$device}";
$o = allspice_get_float_device_options($device);
$style = allspice_get_widget_style($device);
$name = esc_attr(ALLSPICE_SETTINGS_OPTION);
$select = static function (string $key, array $choices, string $current) use ($name): void {
echo '<select name="' . $name . '[' . esc_attr($key) . ']">';
foreach ($choices as $value => $label) {
echo '<option value="' . esc_attr((string)$value) . '" ' .
selected($current, (string)$value, false) . '>' . esc_html($label) . '</option>';
}
echo '</select>';
};
$color = static function (string $key, string $current) use ($name): void {
echo '<input type="text" class="allspice-color-field regular-text" style="max-width:120px" ' .
'name="' . $name . '[' . esc_attr($key) . ']" value="' . esc_attr($current) . '" placeholder="default" />';
};
$check = static function (string $key, bool $current, string $label) use ($name): void {
echo '<input type="hidden" name="' . $name . '[' . esc_attr($key) . ']" value="0" />';
echo '<label style="display:block;margin:3px 0;"><input type="checkbox" value="1" ' .
'name="' . $name . '[' . esc_attr($key) . ']" ' . checked($current, true, false) . '/> ' .
esc_html($label) . '</label>';
};
$row = static function (string $label, callable $body): void {
echo '<tr><th scope="row" style="padding-left:0;width:170px;font-weight:600;">' .
esc_html($label) . '</th><td>';
$body();
echo '</td></tr>';
};
$sel_id = "allspice-style-select{$sx}";
/* Mobile: Overlay or Inline only - floating on a phone is near-fullscreen and re-creates
the ad-coverage problem this architecture exists to avoid. */
$choices = $device === 'mobile'
? array(
'inline' => 'Inline (above the recipe card) — recommended',
'standard' => 'Overlay (classic buttons + full-page widget)',
)
: array(
'floating' => 'Floating (bottom corner card) — recommended',
'inline' => 'Inline (above the recipe card)',
'standard' => 'Overlay (classic buttons + full-page widget)',
);
echo '<select id="' . esc_attr($sel_id) . '" name="' . $name . '[widget_style' . esc_attr($sx) . ']">';
foreach ($choices as $value => $label) {
echo '<option value="' . esc_attr($value) . '" ' . selected($style, $value, false) . '>' .
esc_html($label) . '</option>';
}
echo '</select>';
// ---- The inline "Ask Pico" CTA bar. Optional for Overlay/Floating; a REQUIREMENT of the
// Inline style (it IS that placement's entry point), so the checkbox hides when this
// device is set to Inline - and api.php force-emits it true there regardless of any
// previously stored value. ----
echo '<div style="margin-top:10px;">';
echo '<div id="allspice-grp-cta' . esc_attr($sx) . '" style="' . ($style === 'inline' ? 'display:none;' : '') . '">';
$check("float_inline_cta{$sx}", (bool)$o["float_inline_cta{$sx}"], 'Show the inline "Ask Pico" bar above the recipe card (Recommended)');
echo '<p class="description">The compact entry point with ingredient photos. Tapping it opens ' .
'whatever experience is selected above (Overlay, Floating or Inline).</p>';
echo '</div>';
// ---- Search action target (matters when this device runs Floating or Inline) ----
echo '<div style="margin-top:10px;">';
echo '<label style="display:block;margin-bottom:4px;"><strong>Search buttons open</strong></label>';
$select("search_open_target{$sx}", array(
'full' => 'Full-page widget (recommended)',
'widget' => "This device's widget style (Floating / Inline)",
'advanced_search' => 'Standalone search page on this site (/advanced-search/)',
), $o["search_open_target{$sx}"]);
echo '<p class="description">Where site search buttons land when this device uses Floating or Inline. ' .
'The full-page widget offers the richest search experience; Overlay devices always use it. ' .
'The standalone option navigates to the search page generated under Pages below - a normal page of your site ' .
'with the search experience embedded in it (falls back to the full-page widget while that page is missing or unpublished).</p>';
echo '</div>';
echo '</div>';
// ---- Floating-only: auto-open ----
echo '<div id="allspice-grp-autoopen' . esc_attr($sx) . '" style="margin-top:10px;' . ($style === 'floating' ? '' : 'display:none;') . '">';
$check("float_auto_open{$sx}", (bool)$o["float_auto_open{$sx}"], 'Open the floating widget automatically');
echo '<span style="margin-left:22px;">after <input type="number" min="3" max="600" step="1" style="width:70px" ' .
'name="' . $name . '[float_auto_open_delay' . esc_attr($sx) . ']" value="' . esc_attr((string)$o["float_auto_open_delay{$sx}"]) . '" /> seconds</span>';
echo '</div>';
// ---- Inline + Floating: block chrome. Floating docks the minimized "Ask Pico" bar in
// the SAME inline slot above the recipe card, so its chrome applies there too. ----
echo '<div id="allspice-grp-inline' . esc_attr($sx) . '" style="margin-top:10px;' . (in_array($style, array('inline', 'floating'), true) ? '' : 'display:none;') . '">';
echo '<details style="border:1px solid #dcdcde;border-radius:6px;padding:10px 12px;background:#fff;max-width:640px;">';
echo '<summary style="cursor:pointer;font-weight:600;">Inline block (' . esc_html($device) . ')</summary>';
echo '<div style="margin-top:8px;">';
echo '<p class="description" style="margin:0 0 8px;">Chrome for the block above the recipe card - the Inline widget itself, ' .
'and the minimized "Ask Pico" bar the Floating style places there.</p>';
/* Background stays #ffffff (or a previously stored custom value) via api.php - the
control earned no keep: on white sites it is invisible and the wrong pick on dark
sites broke the card. */
echo 'Border: ';
$color("float_inline_border_color{$sx}", $o["float_inline_border_color{$sx}"]);
echo ' ';
$select("float_inline_border_width{$sx}", array('0' => 'No border', '1' => '1 px', '2' => '2 px'), $o["float_inline_border_width{$sx}"]);
echo ' Corners: ';
$select("float_inline_radius{$sx}", array('0' => 'Square', '8' => '8 px', '12' => '12 px', '16' => '16 px', '24' => '24 px'), $o["float_inline_radius{$sx}"]);
echo '<div style="margin-top:6px;">Top spacing: <input type="number" min="0" max="60" step="1" style="width:70px" ' .
'name="' . $name . '[float_inline_margin_top' . esc_attr($sx) . ']" value="' . esc_attr((string)$o["float_inline_margin_top{$sx}"]) . '" /> px';
echo ' Bottom spacing: <input type="number" min="0" max="60" step="1" style="width:70px" ' .
'name="' . $name . '[float_inline_margin_bottom' . esc_attr($sx) . ']" value="' . esc_attr((string)$o["float_inline_margin_bottom{$sx}"]) . '" /> px</div>';
echo '</div></details>';
echo '</div>';
// ---- Floating + Inline: rail appearance (fully per-device) ----
echo '<div id="allspice-grp-rail' . esc_attr($sx) . '" style="margin-top:10px;' . ($style !== 'standard' ? '' : 'display:none;') . '">';
echo '<details style="border:1px solid #dcdcde;border-radius:6px;padding:10px 12px;background:#fff;max-width:640px;">';
echo '<summary style="cursor:pointer;font-weight:600;">Adaptive rail (' . esc_html($device) . ')</summary>';
echo '<table class="form-table" role="presentation" style="margin-top:6px;"><tbody>';
$row('Position', static function () use ($select, $o, $sx) {
$select("float_launcher_side{$sx}", array('right' => 'Right edge', 'left' => 'Left edge'), $o["float_launcher_side{$sx}"]);
echo ' ';
$select("float_rail_anchor{$sx}", array('bottom' => 'Bottom', 'middle' => 'Middle', 'top' => 'Top'), $o["float_rail_anchor{$sx}"]);
});
$row('Size', static function () use ($select, $o, $sx) {
$select("float_rail_size{$sx}", array('default' => 'Default', 'small' => 'Small'), $o["float_rail_size{$sx}"]);
});
$row('Shell', static function () use ($select, $o, $sx) {
$select("float_rail_mode{$sx}", array('light' => 'Light', 'dark' => 'Dark', 'auto' => 'Auto (match visitor theme)'), $o["float_rail_mode{$sx}"]);
});
$row('Accents', static function () use ($color, $o, $sx) {
foreach (array(
'Jump to Recipe' => "float_rail_accent_top{$sx}",
'Grocery List' => "float_rail_accent_ingredients{$sx}",
'Start Cooking' => "float_rail_accent_instructions{$sx}",
'Search' => "float_rail_accent_search{$sx}",
'Ask Me Anything' => "float_rail_accent_chat{$sx}",
) as $label => $key) {
echo '<div style="display:flex;align-items:center;gap:8px;margin:3px 0;">' .
'<span style="display:inline-block;width:130px;">' . esc_html($label) . '</span>';
$color($key, $o[$key]);
echo '</div>';
}
});
$row('Announce', static function () use ($select, $o, $sx, $name) {
$select("float_rail_announce{$sx}", array('zone' => 'Once per section', 'jump' => 'Only "Jump to Recipe"', 'never' => 'Never'), $o["float_rail_announce{$sx}"]);
echo ' <input type="number" min="0" max="15000" step="100" style="width:90px" name="' . $name .
'[float_rail_announce_ms' . esc_attr($sx) . ']" value="' . esc_attr((string)$o["float_rail_announce_ms{$sx}"]) . '" /> ms';
});
$row('Off-recipe', static function () use ($check, $o, $sx) {
$check("float_rail_announce_search{$sx}", (bool)$o["float_rail_announce_search{$sx}"], 'Announce the search rail on non-recipe pages');
});
$row('Behavior', static function () use ($check, $o, $sx) {
$check("float_rail_pulse{$sx}", (bool)$o["float_rail_pulse{$sx}"], 'Pulse the icon to draw attention');
$check("float_rail_hover_expand{$sx}", (bool)$o["float_rail_hover_expand{$sx}"], 'Expand on hover (desktop pointers only)');
$check("float_rail_show_jump{$sx}", (bool)$o["float_rail_show_jump{$sx}"], 'Show "Jump to Recipe" at the top of the page');
});
echo '</tbody></table></details>';
echo '</div>';
// Live-toggle the groups with the select.
echo '<script>(function(){';
echo 'var sel=document.getElementById(' . wp_json_encode($sel_id) . ');';
echo 'var ao=document.getElementById("allspice-grp-autoopen' . esc_js($sx) . '");';
echo 'var inl=document.getElementById("allspice-grp-inline' . esc_js($sx) . '");';
echo 'var rail=document.getElementById("allspice-grp-rail' . esc_js($sx) . '");';
echo 'var cta=document.getElementById("allspice-grp-cta' . esc_js($sx) . '");';
echo 'if(!sel)return;';
echo 'function s(){var v=sel.value;';
echo 'if(ao)ao.style.display=(v==="floating")?"":"none";';
echo 'if(inl)inl.style.display=(v==="inline"||v==="floating")?"":"none";';
echo 'if(rail)rail.style.display=(v!=="standard")?"":"none";';
echo 'if(cta)cta.style.display=(v==="inline")?"none":"";}';
echo 'sel.addEventListener("change",s);s();';
echo '})();</script>';
}
function allspice_field_widget_style_desktop(): void {
allspice_render_device_style_section('desktop');
}
function allspice_field_widget_style_mobile(): void {
allspice_render_device_style_section('mobile');
}
/*
* Widget Style: per-device Floating/Inline choice + the inline block's chrome. Lives at the top
* level (not inside the Adaptive Rail section) because it decides WHAT renders, not how the rail
* looks. Shown only for the float variant, toggled live with the Widget Experience select.
*/
function allspice_field_float_widget_style(): void {
$o = allspice_get_float_launcher_options();
$name = esc_attr(ALLSPICE_SETTINGS_OPTION);
$is_float = allspice_is_float_variant();
$select = static function (string $key, array $choices, string $current) use ($name): void {
echo '<select name="' . $name . '[' . esc_attr($key) . ']">';
foreach ($choices as $value => $label) {
echo '<option value="' . esc_attr((string)$value) . '" ' .
selected($current, (string)$value, false) . '>' . esc_html($label) . '</option>';
}
echo '</select>';
};
$color = static function (string $key, string $current) use ($name): void {
echo '<input type="text" class="allspice-color-field regular-text" style="max-width:120px" ' .
'name="' . $name . '[' . esc_attr($key) . ']" value="' . esc_attr($current) . '" ' .
'placeholder="default" />';
};
echo '<div id="allspice-float-style-options"' . ($is_float ? '' : ' style="display:none;"') . '>';
echo '<p style="margin:0 0 4px;">Desktop: ';
$select('float_desktop_style', ['floating' => 'Floating (bottom corner overlay)', 'inline' => 'Inline (above the recipe card)'], $o['float_desktop_style']);
echo ' Mobile: ';
$select('float_mobile_style', ['floating' => 'Floating (bottom corner overlay)', 'inline' => 'Inline (above the recipe card)'], $o['float_mobile_style']);
echo '</p>';
echo '<p class="description" style="max-width:640px;">Inline renders the same widget in the page flow above ' .
'the recipe card, so it can never cover floating video or adhesion ads. The rail launcher works with ' .
'both: with Inline it scrolls to the widget instead of opening the overlay.</p>';
echo '<div style="margin-top:10px;"><strong>Inline block</strong></div>';
echo '<p style="margin:4px 0 0;">Background: ';
$color('float_inline_bg', $o['float_inline_bg']);
echo ' Border: ';
$color('float_inline_border_color', $o['float_inline_border_color']);
echo ' ';
$select('float_inline_border_width', ['0' => 'No border', '1' => '1 px', '2' => '2 px'], $o['float_inline_border_width']);
echo '</p><p style="margin:6px 0 0;">Corners: ';
$select('float_inline_radius', ['0' => 'Square', '8' => '8 px', '12' => '12 px', '16' => '16 px', '24' => '24 px'], $o['float_inline_radius']);
echo ' Vertical spacing: ';
$select('float_inline_pad_y', ['8' => 'Compact', '14' => 'Default', '22' => 'Roomy'], $o['float_inline_pad_y']);
echo '</p>';
echo '<p class="description" style="max-width:640px;">Only used when a device is set to the ' .
'<strong>Inline</strong> style - tune these to blend the block into the surrounding page sections.</p>';
echo '</div>';
// Toggle with the variant select, without waiting for a save.
echo '<script>(function(){';
echo 'var sel=document.querySelector(\'select[name="' . $name . '[widget_variant]"]\');';
echo 'var box=document.getElementById("allspice-float-style-options");';
echo 'if(!sel||!box)return;';
echo 'function s(){box.style.display=(sel.value==="float")?"":"none";}';
echo 'sel.addEventListener("change",s);s();';
echo '})();</script>';
}
function allspice_field_float_launcher_options(): void {
$o = allspice_get_float_launcher_options();
$name = esc_attr(ALLSPICE_SETTINGS_OPTION);
$is_float = allspice_is_float_variant();
$select = static function (string $key, array $choices, string $current) use ($name): void {
echo '<select name="' . $name . '[' . esc_attr($key) . ']">';
foreach ($choices as $value => $label) {
echo '<option value="' . esc_attr((string)$value) . '" ' .
selected($current, (string)$value, false) . '>' . esc_html($label) . '</option>';
}
echo '</select>';
};
$color = static function (string $key, string $current) use ($name): void {
echo '<input type="text" class="allspice-color-field regular-text" style="max-width:120px" ' .
'name="' . $name . '[' . esc_attr($key) . ']" value="' . esc_attr($current) . '" ' .
'placeholder="default" />';
};
$check = static function (string $key, bool $current, string $label) use ($name): void {
// Hidden companion so an unchecked box still posts a value (see the sanitizer).
echo '<input type="hidden" name="' . $name . '[' . esc_attr($key) . ']" value="0" />';
echo '<label style="display:block;margin:3px 0;"><input type="checkbox" value="1" ' .
'name="' . $name . '[' . esc_attr($key) . ']" ' . checked($current, true, false) . '/> ' .
esc_html($label) . '</label>';
};
$row = static function (string $label, callable $body): void {
echo '<tr><th scope="row" style="padding-left:0;width:170px;font-weight:600;">' .
esc_html($label) . '</th><td>';
$body();
echo '</td></tr>';
};
echo '<div id="allspice-float-launcher-options"' . ($is_float ? '' : ' style="display:none;"') . '>';
echo '<p class="description" style="margin:0 0 10px;max-width:640px;">The <strong>adaptive rail</strong> ' .
'docks to the screen edge and re-labels itself as the reader moves through the recipe. ' .
'Tapping it opens the floating card. Only applies to the <strong>Floating card</strong> experience.</p>';
echo '<details style="border:1px solid #dcdcde;border-radius:6px;padding:10px 12px;background:#fff;max-width:640px;">';
echo '<summary style="cursor:pointer;font-weight:600;">Rail appearance</summary>';
echo '<table class="form-table" role="presentation" style="margin-top:6px;"><tbody>';
$row('Position', static function () use ($select, $o) {
$select('float_launcher_side', ['right' => 'Right edge', 'left' => 'Left edge'], $o['float_launcher_side']);
echo ' ';
$select('float_rail_anchor', ['bottom' => 'Bottom', 'middle' => 'Middle', 'top' => 'Top'], $o['float_rail_anchor']);
});
$row('Size', static function () use ($select, $o) {
$select('float_rail_size', ['default' => 'Default', 'small' => 'Small'], $o['float_rail_size']);
});
$row('Shell', static function () use ($select, $o) {
$select('float_rail_mode', ['light' => 'Light', 'dark' => 'Dark', 'auto' => 'Auto (match visitor theme)'], $o['float_rail_mode']);
echo '<p class="description">Rail background: white for <strong>Light</strong>, near-black for ' .
'<strong>Dark</strong>. <strong>Auto</strong> follows each visitor’s own light/dark setting.</p>';
});
$row('Accents', static function () use ($color, $o) {
$accent_row = static function (string $label, string $key, string $value) use ($color): void {
echo '<div style="display:flex;align-items:center;gap:8px;margin:3px 0;">' .
'<span style="display:inline-block;width:130px;">' . esc_html($label) . '</span>';
$color($key, $value);
echo '</div>';
};
$accent_row('Jump to Recipe', 'float_rail_accent_top', $o['float_rail_accent_top']);
$accent_row('Grocery List', 'float_rail_accent_ingredients', $o['float_rail_accent_ingredients']);
$accent_row('Start Cooking', 'float_rail_accent_instructions', $o['float_rail_accent_instructions']);
$accent_row('Search', 'float_rail_accent_search', $o['float_rail_accent_search']);
$accent_row('Ask Me Anything', 'float_rail_accent_chat', $o['float_rail_accent_chat']);
echo '<p class="description">Icon chip / stripe color for each rail state. Defaults are the ' .
'built-in palette; set your own to match the site.</p>';
});
$row('Announce', static function () use ($select, $o, $name) {
$select('float_rail_announce', ['zone' => 'Once per section', 'jump' => 'Only "Jump to Recipe"', 'never' => 'Never'], $o['float_rail_announce']);
echo ' <input type="number" min="0" max="15000" step="100" style="width:90px" name="' . $name .
'[float_rail_announce_ms]" value="' . esc_attr((string)$o['float_rail_announce_ms']) . '" /> ms';
echo '<p class="description">On recipe pages: how long the rail opens to its label before ' .
'settling back to the edge tab. Set to 0 to leave it open.</p>';
});
$row('Off-recipe', static function () use ($check, $o) {
$check('float_rail_announce_search', $o['float_rail_announce_search'], 'Announce the search rail on non-recipe pages');
echo '<p class="description">Pages without a recipe show a single <strong>Find a Recipe</strong> ' .
'state. This controls its announcement independently, so recipe pages and the rest of the ' .
'site can behave differently.</p>';
});
$row('Behavior', static function () use ($check, $select, $o) {
$check('float_rail_pulse', $o['float_rail_pulse'], 'Pulse the icon to draw attention');
$check('float_rail_hover_expand', $o['float_rail_hover_expand'], 'Expand on hover (desktop only)');
$check('float_rail_show_jump', $o['float_rail_show_jump'], 'Show "Jump to Recipe" at the top of the page');
});
echo '</tbody></table>';
echo '</details></div>';
// Toggle with the variant select, without waiting for a save.
echo '<script>(function(){';
echo 'var sel=document.querySelector(\'select[name="' . $name . '[widget_variant]"]\');';
echo 'var box=document.getElementById("allspice-float-launcher-options");';
echo 'if(!sel||!box)return;';
echo 'function s(){box.style.display=(sel.value==="float")?"":"none";}';
echo 'sel.addEventListener("change",s);s();';
echo '})();</script>';
}
function allspice_admin_debug_url(string $path = '/'): string {
$nonce = wp_create_nonce(ALLSPICE_DEBUG_NONCE_ACTION);
$base = home_url($path);
return add_query_arg(
[
ALLSPICE_DEBUG_PARAM => '1',
ALLSPICE_DEBUG_NONCE_PARAM => $nonce,
],
$base
);
}
function allspice_render_settings_page(): void {
if (!current_user_can('manage_options')) return;
$s = allspice_opt_get();
echo '<div class="wrap allspice-admin">';
echo '<div class="allspice-admin__stack">';
allspice_render_marketing_section($s);
allspice_admin_card_open('CONFIGURATION', 'Connect WordPress to Allspice', true);
echo '<form method="post" action="options.php">';
settings_fields(ALLSPICE_SETTINGS_GROUP);
do_settings_sections(ALLSPICE_SETTINGS_PAGE);
submit_button('Save Changes');
echo '</form>';
allspice_render_premium_banner($s);
echo '<div class="allspice-admin__divider"></div>';
echo '<p class="allspice-admin__sectionTitle" style="margin-bottom:6px;">Synchronization</p>';
echo '<p class="description" style="max-width:700px;margin-top:0;">Manually sync buttons and recipes from Allspice.</p>';
echo '<div class="allspice-sync-actions">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
wp_nonce_field(ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION) . '"/>';
submit_button('Sync Buttons & Settings', 'secondary', 'submit', false);
echo '</form>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
wp_nonce_field(ALLSPICE_SYNC_RECIPES_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_SYNC_RECIPES_ACTION) . '"/>';
submit_button('Sync Recipes', 'secondary', 'submit', false);
echo '</form>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
wp_nonce_field(ALLSPICE_SYNC_ALL_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_SYNC_ALL_ACTION) . '"/>';
submit_button('Sync All', 'primary', 'submit', false);
echo '</form>';
echo '</div>';
/* --------------------------------------------------- Pages (generated by Allspice) */
echo '<div class="allspice-admin__divider"></div>';
echo '<p class="allspice-admin__sectionTitle" style="margin-bottom:6px;">Pages</p>';
echo '<p class="description" style="max-width:700px;margin-top:0;">Standalone pages generated by Allspice. Created as drafts - review, customize, and publish them yourself.</p>';
echo '<div class="allspice-sync-actions">';
$membership_page = function_exists('allspice_membership_page_current') ? allspice_membership_page_current() : null;
if ($membership_page !== null) {
/* A valid page exists: link straight into the editor, and offer a confirmed
regenerate (old page goes to Trash - recoverable, never silently overwritten). */
echo '<a class="button button-secondary" href="'
. esc_url(allspice_membership_page_editor_url((int)$membership_page->ID))
. '">Edit Membership Page</a>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '"'
. ' onsubmit="return confirm(\'Regenerate the default membership page? Your current membership page (including any edits) will be moved to the Trash and a fresh draft will be created from the latest default layout and pricing.\');">';
wp_nonce_field(ALLSPICE_REGENERATE_MEMBERSHIP_PAGE_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_REGENERATE_MEMBERSHIP_PAGE_ACTION) . '"/>';
submit_button('Regenerate Default Membership Page', 'secondary', 'submit', false);
echo '</form>';
} else {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
wp_nonce_field(ALLSPICE_CREATE_MEMBERSHIP_PAGE_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_CREATE_MEMBERSHIP_PAGE_ACTION) . '"/>';
submit_button('Create Membership Page', 'secondary', 'submit', false);
echo '</form>';
}
$search_page = function_exists('allspice_search_page_current') ? allspice_search_page_current() : null;
if ($search_page !== null) {
echo '<a class="button button-secondary" href="'
. esc_url(allspice_membership_page_editor_url((int)$search_page->ID))
. '">Edit Search Page</a>';
/* Page content is stamped at creation, so an existing page never picks up a new default
header on its own - regenerate is the only path (old page to Trash, recoverable). */
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '"'
. ' onsubmit="return confirm(\'Regenerate the default search page? Your current search page (including any edits) will be moved to the Trash and a fresh draft will be created from the latest default layout.\');">';
wp_nonce_field(ALLSPICE_REGENERATE_SEARCH_PAGE_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_REGENERATE_SEARCH_PAGE_ACTION) . '"/>';
submit_button('Regenerate Default Search Page', 'secondary', 'submit', false);
echo '</form>';
if ($search_page->post_status !== 'publish') {
echo '<span class="description" style="align-self:center;">Search page is a draft - publish it before selecting "Advanced Search page" above.</span>';
}
} else {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
wp_nonce_field(ALLSPICE_CREATE_SEARCH_PAGE_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_CREATE_SEARCH_PAGE_ACTION) . '"/>';
submit_button('Create Search Page', 'secondary', 'submit', false);
echo '</form>';
}
$mealplans_page = function_exists('allspice_mealplans_page_current') ? allspice_mealplans_page_current() : null;
if ($mealplans_page !== null) {
echo '<a class="button button-secondary" href="'
. esc_url(allspice_mealplans_page_editor_url((int)$mealplans_page->ID))
. '">Edit Meal Plans Page</a>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '"'
. ' onsubmit="return confirm(\'Regenerate the default meal plans page? Your current meal plans page (including any edits) will be moved to the Trash and a fresh draft will be created from the latest default layout.\');">';
wp_nonce_field(ALLSPICE_REGENERATE_MEALPLANS_PAGE_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_REGENERATE_MEALPLANS_PAGE_ACTION) . '"/>';
submit_button('Regenerate Default Meal Plans Page', 'secondary', 'submit', false);
echo '</form>';
} else {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
wp_nonce_field(ALLSPICE_CREATE_MEALPLANS_PAGE_ACTION);
echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_CREATE_MEALPLANS_PAGE_ACTION) . '"/>';
submit_button('Create Meal Plans Page', 'secondary', 'submit', false);
echo '</form>';
}
echo '</div>';
/* Optional-migration notice for shortcode-era membership pages: they keep working as-is;
Regenerate produces a fully editable Gutenberg version (old page goes to Trash). */
if ($membership_page !== null
&& function_exists('allspice_membership_page_is_legacy_shortcode')
&& allspice_membership_page_is_legacy_shortcode((string)$membership_page->post_content)) {
echo '<p class="description" style="max-width:700px;">Your membership page currently uses the legacy <code>[allspice_membership]</code> shortcode. It continues to work - but you can click <strong>Regenerate Default Membership Page</strong> to get a fully editable Gutenberg version (the current page is moved to the Trash, not deleted).</p>';
}
allspice_admin_card_close();
allspice_admin_card_open('ADVANCED', 'Status & Debug', false);
$fmt = get_option('date_format') . ' ' . get_option('time_format');
echo '<table class="widefat striped" style="margin-top:6px;">';
echo '<tr><th>Linked Buttons</th><td>' . esc_html((string)($s['buttons_count'] ?? 0)) . '</td></tr>';
echo '<tr><th>Last Manual Refresh</th><td>' . esc_html(!empty($s['manual_refreshed_at']) ? wp_date($fmt, (int)$s['manual_refreshed_at']) : 'Never') . '</td></tr>';
echo '<tr><th>Last Cache Refresh</th><td>' . esc_html(!empty($s['cache_fetched_at']) ? wp_date($fmt, (int)$s['cache_fetched_at']) : 'Never') . '</td></tr>';
echo '<tr><th>Cache TTL</th><td>' . esc_html(round(((int)($s['cache_ttl'] ?? 0)) / 3600, 2)) . ' hours</td></tr>';
$cg_stored = get_option('allspice_content_gates_v1');
$cg_count = (is_array($cg_stored) && isset($cg_stored['gates']) && is_array($cg_stored['gates'])) ? count($cg_stored['gates']) : 0;
echo '<tr><th>Content Gates</th><td>' . esc_html((string)$cg_count) . '</td></tr>';
echo '<tr><th>Last Content-Gate Sync</th><td>' . esc_html(!empty($s['content_gates_synced_at']) ? wp_date($fmt, (int)$s['content_gates_synced_at']) : 'Never') . '</td></tr>';
echo '<tr><th>Content-Gate Error</th><td>' . (!empty($s['content_gates_last_error']) ? esc_html($s['content_gates_last_error']) : '<em>None</em>') . '</td></tr>';
$recipes_count = (int)($s['recipes_count'] ?? 0);
$recipes_synced_at = (int)($s['recipes_synced_at'] ?? 0);
$recipes_last_error = (string)($s['recipes_last_error'] ?? '');
echo '<tr><th>Synced Recipes</th><td>' . esc_html((string)$recipes_count) . '</td></tr>';
echo '<tr><th>Last Recipe Sync</th><td>' . esc_html($recipes_synced_at ? wp_date($fmt, $recipes_synced_at) : 'Never') . '</td></tr>';
$tp_synced_at = (int)($s['theme_policies_synced_at'] ?? 0);
$tp_err = (string)($s['theme_policies_last_error'] ?? '');
$tp = function_exists('allspice_theme_policies_cache_get') ? allspice_theme_policies_cache_get() : ['theme' => null, 'policy' => null];
$theme_synced = ($tp_synced_at > 0) && ($tp['theme'] !== null);
$policies_synced = ($tp_synced_at > 0) && ($tp['policy'] !== null);
echo '<tr><th>Theme</th><td>' . ($theme_synced ? '<strong>Synced</strong>' : '<em>Not synced</em>') . '</td></tr>';
echo '<tr><th>Policies</th><td>' . ($policies_synced ? '<strong>Synced</strong>' : '<em>Not synced</em>') . '</td></tr>';
echo '<tr><th>Last Theme/Policies Sync</th><td>' . esc_html($tp_synced_at ? wp_date($fmt, (int)$tp_synced_at) : 'Never') . '</td></tr>';
echo '<tr><th>Manual Error</th><td>' . (!empty($s['manual_last_error']) ? esc_html($s['manual_last_error']) : '<em>None</em>') . '</td></tr>';
echo '<tr><th>Cache Error</th><td>' . (!empty($s['cache_last_error']) ? esc_html($s['cache_last_error']) : '<em>None</em>') . '</td></tr>';
echo '<tr><th>Recipe Sync Error</th><td>' . (!empty($recipes_last_error) ? esc_html($recipes_last_error) : '<em>None</em>') . '</td></tr>';
echo '<tr><th>Theme/Policies Sync Error</th><td>' . (!empty($tp_err) ? esc_html($tp_err) : '<em>None</em>') . '</td></tr>';
$premium = (bool)($s['premium'] ?? true);
$premium_checked_at = (int)($s['premium_checked_at'] ?? 0);
echo '<tr><th>Premium</th><td>' . ($premium ? '<strong>Active</strong>' : '<strong style="color:#b42318;">Not active</strong>') . '</td></tr>';
echo '<tr><th>Last Premium Check</th><td>' . esc_html($premium_checked_at ? wp_date($fmt, $premium_checked_at) : 'Never') . '</td></tr>';
echo '</table>';
echo '<div class="allspice-admin__divider"></div>';
echo '<p class="allspice-admin__sectionTitle" style="margin-bottom:6px;">Debug</p>';
echo '<p class="description" style="max-width:700px;margin-top:0;">Debug logging is restricted to administrators and requires a nonce. Use this link to enable debug mode on the front end.</p>';
$debug_url = allspice_admin_debug_url('/');
echo '<p style="margin:10px 0 8px 0;"><input type="text" class="large-text code" readonly value="' . esc_attr($debug_url) . '" /></p>';
echo '<p style="margin:0;"><a class="button button-secondary" href="' . esc_url($debug_url) . '" target="_blank" rel="noopener noreferrer">Open Debug Mode</a></p>';
allspice_admin_card_close();
echo '</div>';
echo '</div>';
}
function allspice_admin_post_sync_buttons_settings(): void {
if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
check_admin_referer(ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION);
$now = time();
$rb = allspice_refresh_buttons_cache(true);
if (empty($rb['ok'])) {
allspice_opt_update(['manual_last_error' => (string)($rb['error'] ?? 'Unknown error')]);
set_transient('allspice_admin_notice', [
'msg' => 'Sync failed (buttons): ' . (string)($rb['error'] ?? 'Unknown error'),
'type' => 'error'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
$rtp = allspice_refresh_theme_policies_cache(true);
if (empty($rtp['ok'])) {
allspice_opt_update([
'manual_refreshed_at' => $now,
'manual_last_error' => '',
'theme_policies_last_error' => (string)($rtp['error'] ?? 'Unknown error'),
'theme_policies_synced_at' => $now,
]);
set_transient('allspice_admin_notice', [
'msg' => 'Buttons synced, but settings sync failed: ' . (string)($rtp['error'] ?? 'Unknown error'),
'type' => 'error'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
allspice_opt_update(['manual_refreshed_at' => $now, 'manual_last_error' => '']);
$buttons_count = (int)($rb['count'] ?? (allspice_opt_get()['buttons_count'] ?? 0));
$theme_ok = !empty($rtp['theme_ok']);
$pol_ok = !empty($rtp['policy_ok']);
/* Content-access rules ride the same manual sync; failure NEVER clears the previous
snapshot, and the notice says exactly which of the three outcomes happened. */
$rcg = function_exists('allspice_refresh_content_gates') ? allspice_refresh_content_gates(true) : ['ok' => false, 'error' => 'unavailable'];
if (!empty($rcg['ok'])) {
$content_msg = !empty($rcg['changed'])
? 'Content access rules synced: ' . (int)($rcg['count'] ?? 0)
: 'Content access rules: no changes';
} else {
$content_msg = 'Content access sync failed; previous snapshot retained';
}
set_transient('allspice_admin_notice', [
'msg' => 'Buttons & settings synced successfully. Buttons: ' . $buttons_count .
' | Theme: ' . ($theme_ok ? 'synced' : 'missing') .
' | Policies: ' . ($pol_ok ? 'synced' : 'missing') .
' | ' . $content_msg,
'type' => 'success'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
function allspice_admin_post_sync_recipes(): void {
if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
check_admin_referer(ALLSPICE_SYNC_RECIPES_ACTION);
$now = time();
$rr = allspice_refresh_recipes_cache(true);
if (empty($rr['ok'])) {
allspice_opt_update([
'recipes_last_error' => (string)($rr['error'] ?? 'Unknown error'),
'recipes_synced_at' => $now,
]);
set_transient('allspice_admin_notice', [
'msg' => 'Recipe sync failed: ' . (string)($rr['error'] ?? 'Unknown error'),
'type' => 'error'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
$recipe_count = (int)($rr['count'] ?? 0);
$premium = (bool)($rr['premium'] ?? true);
set_transient('allspice_admin_notice', [
'msg' => 'Recipes synced successfully. Recipes: ' . $recipe_count .
($premium ? '' : ' | Premium is not active for this domain.'),
'type' => $premium ? 'success' : 'warning'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
function allspice_admin_post_sync_all(): void {
if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
check_admin_referer(ALLSPICE_SYNC_ALL_ACTION);
$now = time();
$rb = allspice_refresh_buttons_cache(true);
if (empty($rb['ok'])) {
allspice_opt_update(['manual_last_error' => (string)($rb['error'] ?? 'Unknown error')]);
set_transient('allspice_admin_notice', [
'msg' => 'Sync failed (buttons): ' . (string)($rb['error'] ?? 'Unknown error'),
'type' => 'error'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
allspice_opt_update(['manual_refreshed_at' => $now, 'manual_last_error' => '']);
$rr = allspice_refresh_recipes_cache(true);
if (empty($rr['ok'])) {
allspice_opt_update([
'recipes_last_error' => (string)($rr['error'] ?? 'Unknown error'),
'recipes_synced_at' => $now,
]);
set_transient('allspice_admin_notice', [
'msg' => 'Buttons synced, but recipe sync failed: ' . (string)($rr['error'] ?? 'Unknown error'),
'type' => 'error'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
$rtp = allspice_refresh_theme_policies_cache(true);
if (empty($rtp['ok'])) {
allspice_opt_update([
'theme_policies_last_error' => (string)($rtp['error'] ?? 'Unknown error'),
'theme_policies_synced_at' => $now,
]);
set_transient('allspice_admin_notice', [
'msg' => 'Buttons + recipes synced, but settings sync failed: ' . (string)($rtp['error'] ?? 'Unknown error'),
'type' => 'error'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
$rcg = function_exists('allspice_refresh_content_gates') ? allspice_refresh_content_gates(true) : ['ok' => false, 'error' => 'unavailable'];
$content_msg = !empty($rcg['ok'])
? (!empty($rcg['changed']) ? 'Content gates: ' . (int)($rcg['count'] ?? 0) : 'Content gates: no changes')
: 'Content gates: sync failed; previous snapshot retained';
$buttons_count = (int)($rb['count'] ?? (allspice_opt_get()['buttons_count'] ?? 0));
$recipe_count = (int)($rr['count'] ?? 0);
$premium = (bool)($rr['premium'] ?? true);
$theme_ok = !empty($rtp['theme_ok']);
$pol_ok = !empty($rtp['policy_ok']);
set_transient('allspice_admin_notice', [
'msg' => 'Synced successfully. ' . $content_msg . ' | Buttons: ' . $buttons_count .
' | Recipes: ' . $recipe_count .
' | Theme: ' . ($theme_ok ? 'synced' : 'missing') .
' | Policies: ' . ($pol_ok ? 'synced' : 'missing') .
($premium ? '' : ' | Premium is not active for this domain.'),
'type' => $premium ? 'success' : 'warning'
], 30);
wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
exit;
}
add_action('admin_post_' . ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION, 'allspice_admin_post_sync_buttons_settings');
add_action('admin_post_' . ALLSPICE_SYNC_RECIPES_ACTION, 'allspice_admin_post_sync_recipes');
add_action('admin_post_' . ALLSPICE_SYNC_ALL_ACTION, 'allspice_admin_post_sync_all');